﻿using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;

namespace SharePoint.UserAccountControl
{
    /// <summary>
    /// 
    /// </summary>
    public class SharePointAccess
    {
        /// <summary>
        /// Checks the farm.
        /// </summary>
        /// <param name="userName">The user name.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>       
        public List<UserAccessInfo> CheckFarm(string userName)
        {
            Log("CheckFarm('{0}')", userName);

            // Get information about the user
            var userInfo = Security.GetUserInfo(userName);

            // Get the farm
            var farm = SPFarm.Local;
            
            var accessList = new List<UserAccessInfo>();

            // Check central administration
            accessList.AddRange(CheckCentralAdministration(userInfo, farm));

            // Check each web application
            foreach (var application in SPWebService.ContentService.WebApplications)
            {
                accessList.AddRange(CheckWebApplication(userInfo, application));
            }

            return accessList;
        }

        /// <summary>
        /// Checks central administration.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="farm">The farm.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>
        public List<UserAccessInfo> CheckCentralAdministration(UserInfo userInfo, SPFarm farm)
        {
            Log("CheckCentralAdministration()");

            var accessList = new List<UserAccessInfo>();

            // Get the central admin web application
            var application = SPAdministrationWebApplication.Local.Sites["/"].WebApplication;

            accessList.AddRange(CheckWebApplication(userInfo, application));

            return accessList;
        }

        /// <summary>
        /// Checks the web application.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="application">The application.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>       
        public List<UserAccessInfo> CheckWebApplication(UserInfo userInfo, SPWebApplication application)
        {
            var accessList = new List<UserAccessInfo>();

            // Check application policies
            accessList.AddRange(CheckApplicationPolicies(userInfo, application));

            // Check each site collection
            foreach (SPSite site in application.Sites)
            {
                try
                {
                    accessList.AddRange(CheckSiteCollection(userInfo, site));
                }
                finally
                {
                    site.Dispose();
                }
            }

            return accessList;
        }
        
        /// <summary>
        /// Checks the application policies.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="application">The application.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>
        public List<UserAccessInfo> CheckApplicationPolicies(UserInfo userInfo, SPWebApplication application)
        {
            Log("CheckApplicationPolicies()");

            var accessList = new List<UserAccessInfo>();

            foreach (SPPolicy policy in application.Policies)
            {
                accessList.AddRange(CheckApplicationPolicy(userInfo, policy, application));
            }

            return accessList;
        }

        /// <summary>
        /// Checks the application policy.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="policy">The policy.</param>
        /// <param name="application">The web application.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>
        public List<UserAccessInfo> CheckApplicationPolicy(UserInfo userInfo, SPPolicy policy, SPWebApplication application)
        {
            Log("CheckApplicationPolicy()");

            var accessList = new List<UserAccessInfo>();

            // Get the effective permissions for this role
            var effectiveGrantPermissions = SPBasePermissions.EmptyMask;
            foreach (SPPolicyRole role in policy.PolicyRoleBindings)
            {
                effectiveGrantPermissions = effectiveGrantPermissions | role.GrantRightsMask;
            }

            // If there are no grants for this policy, return
            if (effectiveGrantPermissions == SPBasePermissions.EmptyMask)
            {
                return accessList;
            }

            // Check the policy user
            if (CheckPolicyUser(userInfo, policy))
            {
                var accessInfo = new UserAccessInfo
                                     {
                                         Description =
                                             string.Format("Granted '{0}' to '{1}' via application policy for '{2}'",
                                                           policy.PolicyRoleBindings.ToCommaSeparatedList(),
                                                           application,
                                                           policy.UserName)
                                     };

                accessList.Add(accessInfo);
                Log(accessInfo.Description);
            }
            
            return accessList;
        }

        /// <summary>
        /// Checks the site collection.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="site">The site.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>
        public List<UserAccessInfo> CheckSiteCollection(UserInfo userInfo, SPSite site)
        {
            Log("CheckSiteCollection()");

            var accessList = new List<UserAccessInfo>();

            // Check site owners
            accessList.AddRange(CheckSiteCollectionOwners(userInfo, site));

            // Check site collection administrators
            accessList.AddRange(CheckSiteCollectionAdministrators(userInfo, site));
            
            // Check each web
            foreach(SPWeb web in site.AllWebs)
            {
                try
                {
                    accessList.AddRange(CheckWeb(userInfo, web));
                }
                finally
                {
                    web.Dispose();
                }
            }

            return accessList;
        }

        /// <summary>
        /// Checks the site collection owners.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="site">The site.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>
        public List<UserAccessInfo> CheckSiteCollectionOwners(UserInfo userInfo, SPSite site)
        {
            Log("CheckSiteCollectionOwners()");

            var accessList = new List<UserAccessInfo>();

            // Check primary and secondary owner
            if (CheckUser(userInfo, site.Owner) || CheckUser(userInfo, site.SecondaryContact))
            {
                var accessInfo = new UserAccessInfo
                {
                    Description =
                        string.Format("Granted '{0}' to '{1}'",
                        "Site Collection Owner",
                        site.Url)
                };
                accessList.Add(accessInfo);
                Log(accessInfo.Description);

            }

            return accessList;
        }

        /// <summary>
        /// Checks the site collection administrators.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="site">The site.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>
        public List<UserAccessInfo> CheckSiteCollectionAdministrators(UserInfo userInfo, SPSite site)
        {
            Log("CheckSiteCollectionAdministrators()");
            
            var accessList = new List<UserAccessInfo>();

            // Get the web
            using (var web = site.OpenWeb())
            {
                // Site collection administrators cannot be a group
                foreach (SPUser user in web.SiteAdministrators)
                {
                    if (CheckUser(userInfo, user))
                    {
                        var accessInfo = new UserAccessInfo
                                             {
                                                 Description =
                                                     string.Format("Granted '{0}' to '{1}' via '{2}'",
                                                                   "Site Collection Administrator",
                                                                   web.Url,
                                                                   user.LoginName)
                                             };

                        accessList.Add(accessInfo);
                        Log(accessInfo.Description);
                    }
                }
            }

            return accessList;
        }

        /// <summary>
        /// Checks the web.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="web">The web.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>
        public List<UserAccessInfo> CheckWeb(UserInfo userInfo, SPWeb web)
        {
            Log("CheckWeb()");

            var accessList = new List<UserAccessInfo>();

            // IF a user is given permissions anywhere within a site, they will be in the site collection users
            // If a group is given permissions in the site, it will not be visible in the site collection users (but is there)
            foreach (SPUser siteUser in web.SiteUsers)
            {
                if (CheckUser(userInfo, siteUser))
                {
                    //accessList.Add(new UserAccessInfo
                    //                   {
                    //                       Description =
                    //                           string.Format("''{0}' is site collection user in '{1}'",
                    //                                         siteUser.LoginName, siteUser.ParentWeb)
                    //                   });
                }
            }

            // Check the role assignments
            // If a group is given permissions to a specific securable, they can be found in the role assignments
            foreach(SPRoleAssignment roleAssignment in web.RoleAssignments)
            {
                accessList.AddRange(CheckRoleAssignment(userInfo, roleAssignment));
            }

            return accessList;
        }
        
        /// <summary>
        /// Checks the role assignment.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="roleAssignment">The role assignment.</param>
        /// <returns>A list of <see cref="UserAccessInfo"/>.</returns>
        public List<UserAccessInfo> CheckRoleAssignment(UserInfo userInfo, SPRoleAssignment roleAssignment)
        {
            Log("CheckRoleAssignment()");

            var accessList = new List<UserAccessInfo>();

            // SharePoint user
            if (roleAssignment.Member is SPUser)
            {
                var user = roleAssignment.Member as SPUser;
                if (CheckUser(userInfo, user))
                {
                    var accessInfo = new UserAccessInfo
                                         {
                                             Description =
                                                 string.Format("Granted '{0}' to '{1}' via '{2}'",
                                                 roleAssignment.RoleDefinitionBindings.ToCommaSeparatedList(),
                                                 ((SPWeb)roleAssignment.Parent).Url,
                                                 user.LoginName)
                                         };
                    accessList.Add(accessInfo);
                    Log(accessInfo.Description);
                }
            }

            // SharePoint grouop
            if(roleAssignment.Member is SPGroup)
            {
                var group = roleAssignment.Member as SPGroup;
                foreach(SPUser user in group.Users)
                {
                    if(CheckUser(userInfo, user))
                    {
                        var accessInfo = new UserAccessInfo
                        {
                            Description =
                                string.Format("Granted '{0}' to '{1}' via '{2}' in SharePoint group '{3}'",
                                roleAssignment.RoleDefinitionBindings.ToCommaSeparatedList(),
                                ((SPWeb)roleAssignment.Parent).Url,
                                user.LoginName,
                                group.Name)
                        };
                        accessList.Add(accessInfo);
                        Log(accessInfo.Description);
                    }
                }
            }

            return accessList;

        }
        
        /// <summary>
        /// Checks the user.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="user">The user.</param>
        /// <returns>True if the user is the same as the SharePoint user or if the SharePoint user is in one of the user's groups.</returns>
        public bool CheckUser(UserInfo userInfo, SPUser user)
        {
            if(user == null)
            {
                return false;
            }

            if (user.IsDomainGroup)
            {
                // Domain group: check each user the group to see if it's the user
                if (Security.CompareSid(userInfo.GroupSids, user.Sid))
                {
                    Log("Found user in site {0}, domain group {1}", user.ParentWeb.Url, user.LoginName);
                    return true;
                }
            }
            else
            {
                // Domain user: compare sids
                if (Security.CompareSid(userInfo.Sid, user.Sid))
                {
                    Log("Found user in site {0}", user.ParentWeb.Url);
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Checks the policy user.
        /// </summary>
        /// <param name="userInfo">The user info.</param>
        /// <param name="policy">The policy.</param>
        /// <returns>True if the user is granted access by the policy.</returns>
        public bool CheckPolicyUser(UserInfo userInfo, SPPolicy policy)
        {
            UserInfo policyUserInfo;
            try
            {
                // Get the user or group
                policyUserInfo = Security.GetUserInfo(policy.UserName);
            }
            catch (Exception ex)
            {
                Log("Failed to get policy user '{0}'. {1}", policy.UserName, ex);
                return false;
            }

            // Check if the user is the policy user
            if (Security.CompareSid(userInfo.Sid, policyUserInfo.Sid))
            {
                return true;
            }

            // Check if the one of the user's groups is is the policy user
            if (Security.CompareSid(userInfo.GroupSids, policyUserInfo.Sid))
            {
                return true;
            }

            //TODO: this isn't working
            // Check if the one of the user's groups is is the policy user's groups
            //if (Security.CompareSid(userInfo.GroupSids, policyUserInfo.GroupSids))
            //{
            //    return true;
            //}

            return false;
        }

        /// <summary>
        /// Logs the specified message.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <param name="args">The args.</param>
        private void Log(string message, params object[] args)
        {
            //Console.WriteLine(message, args);
        }
    }

    /// <summary>
    /// Extension methods for <see cref="SharePointAccess"/>.
    /// </summary>
    public static class SharePointAccessExtensionMethods
    {
        /// <summary>
        /// Converts the list to a comma-separated list of role names.
        /// </summary>
        /// <param name="roleDefinitionBindings">The role definition bindings.</param>
        /// <returns>A comma-separated list of role names.</returns>
        public static string ToCommaSeparatedList(this SPRoleDefinitionBindingCollection roleDefinitionBindings)
        {
            var list = new StringBuilder();
            var requiresComma = false;
            foreach (SPRoleDefinition definition in roleDefinitionBindings)
            {
                list.AppendFormat("{0}{1}", requiresComma ? ", " : string.Empty, definition.Name);
                requiresComma = true;
            }
            return list.ToString();
        }

        /// <summary>
        /// Converts the list to a comma-separated list of role names.
        /// </summary>
        /// <param name="policyRoleBindingCollection">The role definition bindings.</param>
        /// <returns>A comma-separated list of role names.</returns>
        public static string ToCommaSeparatedList(this SPPolicy.SPPolicyRoleBindingCollection policyRoleBindingCollection)
        {
            var list = new StringBuilder();
            var requiresComma = false;
            foreach (SPPolicyRole definition in policyRoleBindingCollection)
            {
                list.AppendFormat("{0}{1}", requiresComma ? ", " : string.Empty, definition.Name);
                requiresComma = true;
            }
            return list.ToString();
        }
    }
}
